﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.Json.Serialization;

namespace ShkSimulation.core.util {
	/// <summary>
	/// Represents the definition of a dynamic property which can be added to an object at runtime.
	/// </summary>
	public class DynamicProperty {
		/// <summary>
		/// The Name of the property.
		/// </summary>
		public string PropertyName { get; set; }

		/// <summary>
		/// The Display Name of the property for the end-user.
		/// </summary>
		public string DisplayName { get; set; }

		/// <summary>
		/// The Name of the underlying System Type of the property.
		/// </summary>
		public string SystemTypeName { get; set; }

		/// <summary>
		/// The underlying System Type of the property.
		/// </summary>
		[JsonIgnore]
		public Type SystemType => Type.GetType(SystemTypeName);
	}

	public static class StringExtensions {
		#region Methods

		/// <summary>
		/// Converts the specified name to camel case.
		/// </summary>
		/// <param name="name">The name to convert</param>
		/// <returns>A camel cased version of the specified name</returns>
		public static string ToCamelCase(this string name) {
			if (name == null) throw new ArgumentNullException(nameof(name));

			return string.Join(".",
				name.Split('.').Select(n => char.ToLower(n[0], CultureInfo.InvariantCulture) + n.Substring(1)));
		}

		#endregion
	}

	/// <summary>
	/// Generates new Types with dynamically added properties.
	/// </summary>
	public class DynamicTypeFactory {
		#region Fields

		private TypeBuilder _typeBuilder;

		#endregion

		#region Readonlys

		private readonly AssemblyBuilder _assemblyBuilder;
		private readonly ModuleBuilder _moduleBuilder;

		#endregion

		#region Constructor

		/// <summary>
		/// Constructor.
		/// </summary>
		public DynamicTypeFactory() {
			var uniqueIdentifier = Guid.NewGuid().ToString();
			var assemblyName = new AssemblyName(uniqueIdentifier);

			_assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect);
			_moduleBuilder = _assemblyBuilder.DefineDynamicModule(uniqueIdentifier);
		}

		#endregion

		#region Methods

		#region Public

		/// <summary>
		/// Creates a new Type based on the specified parent Type and attaches dynamic properties.
		/// </summary>
		/// <param name="parentType">The parent Type to base the new Type on</param>
		/// <param name="dynamicProperties">The collection of dynamic properties to attach to the new Type</param>
		/// <returns>An extended Type with dynamic properties added to it</returns>
		public Type CreateNewTypeWithDynamicProperties(Type parentType,
			IEnumerable<DynamicProperty> dynamicProperties) {
			_typeBuilder =
				_moduleBuilder.DefineType(parentType.Name + Guid.NewGuid().ToString(), TypeAttributes.Public);
			_typeBuilder.SetParent(parentType);

			foreach (DynamicProperty property in dynamicProperties)
				AddDynamicPropertyToType(property);

			return _typeBuilder.CreateType();
		}

		#endregion

		#region Private

		/// <summary>
		/// Adds the specified dynamic property to the new type.
		/// </summary>
		/// <param name="dynamicProperty">The definition of the dynamic property to add to the Type</param>
		private void AddDynamicPropertyToType(DynamicProperty dynamicProperty) {
			Type propertyType = dynamicProperty.SystemType;
			string propertyName = $"{nameof(DynamicProperty)}_{dynamicProperty.PropertyName}";
			string fieldName = $"_{propertyName.ToCamelCase()}";

			FieldBuilder fieldBuilder = _typeBuilder.DefineField(fieldName, propertyType, FieldAttributes.Private);

			// The property set and get methods require a special set of attributes.
			MethodAttributes getSetAttributes =
				MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

			// Define the 'get' accessor method.
			MethodBuilder getMethodBuilder =
				_typeBuilder.DefineMethod($"get_{propertyName}", getSetAttributes, propertyType, Type.EmptyTypes);
			ILGenerator propertyGetGenerator = getMethodBuilder.GetILGenerator();
			propertyGetGenerator.Emit(OpCodes.Ldarg_0);
			propertyGetGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
			propertyGetGenerator.Emit(OpCodes.Ret);

			// Define the 'set' accessor method.
			MethodBuilder setMethodBuilder = _typeBuilder.DefineMethod($"set_{propertyName}", getSetAttributes, null,
				new Type[] {propertyType});
			ILGenerator propertySetGenerator = setMethodBuilder.GetILGenerator();
			propertySetGenerator.Emit(OpCodes.Ldarg_0);
			propertySetGenerator.Emit(OpCodes.Ldarg_1);
			propertySetGenerator.Emit(OpCodes.Stfld, fieldBuilder);
			propertySetGenerator.Emit(OpCodes.Ret);

			// Lastly, we must map the two methods created above to a PropertyBuilder and their corresponding behaviors, 'get' and 'set' respectively.
			PropertyBuilder propertyBuilder =
				_typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
			propertyBuilder.SetGetMethod(getMethodBuilder);
			propertyBuilder.SetSetMethod(setMethodBuilder);

			// Add a 'DisplayName' attribute.
			var attributeType = typeof(DisplayNameAttribute);
			var attributeBuilder = new CustomAttributeBuilder(
				attributeType.GetConstructor(new Type[] {typeof(string)}), // Constructor selection.
				new object[] {dynamicProperty.DisplayName}, // Constructor arguments.
				new PropertyInfo[] { }, // Properties to assign to.                    
				new object[] { } // Values for property assignment.
			);
			propertyBuilder.SetCustomAttribute(attributeBuilder);
		}

		#endregion

		#endregion
	}
}